我的 Dokploy 迁移实战完整记录

一台跑着 1Panel + OpenResty 的服务器, 一堆已经在跑的站点, 再加一个我想尝试的 Dokploy—— 故事就从端口冲突开始。


一、起点:一台已经“有历史”的服务器

在折腾 Dokploy 之前,我的服务器大概是这个状态:

  • 1Panel 管理服务器和应用;
  • OpenResty(Nginx 变种) 做统一反向代理,占用 80443 端口;
  • /opt/1panel/apps/openresty/openresty/conf/conf.d/ 下面,配置了一堆站点:

  • wenzhixuan.com

  • todo.626909.xyz
  • chat.626909.xyz
  • chat.townboats.me
  • aim.626909.xyz
  • newapi.626909.xyz
  • 以及其它一些小玩具站;
  • SSL 证书由 1Panel + acme.sh + OpenResty 自动续期,已经跑得挺稳定。

我的诉求很简单:

想用 Dokploy 体验一种更“云原生”的部署方式, 最好还能顺带把这些服务都慢慢统一起来。

于是我开始了 Dokploy 安装之旅。


二、第一轮博弈:端口、OpenResty 和 Dokploy

1. Dokploy 的“强需求”:80 / 443 / 3000 必须空

Dokploy 的安装脚本非常硬核:

  • 80 端口:给 Traefik 做 HTTP 入口 + ACME HTTP-01 验证
  • 443 端口:给 Traefik 做 HTTPS 入口
  • 3000 端口:Dokploy Web 面板

脚本在跑之前会直接检查:

ss -tulnp | grep ':80 '
ss -tulnp | grep ':443 '
ss -tulnp | grep ':3000 '

任何一个被占用就直接退出。

实际情况是:

  • 80 / 443 被 OpenResty 占着;
  • 3000 还被某个 docker-proxy 映射占了。

于是我进入了“先挪开旧东西”的阶段。

2. 把 OpenResty 改到 8080/8443:第一次大手术

我通过 1Panel 装的 OpenResty,没有独立的 systemd 服务名,全靠 1Panel 管理。配置路径大概是:

/opt/1panel/apps/openresty/openresty/conf/nginx.conf
/opt/1panel/apps/openresty/openresty/conf/conf.d/*.conf

我先备份了配置,然后用 sed + grep 批量改端口。途中还踩了好几次坑:

  • 有的写法是 listen 80;
  • 有的是 listen 80 ;(多一个空格)
  • sed 一开始没匹配到,导致一部分配置没改成功;
  • 结果就是:80 和 8080 同时被 OpenResty 监听,我以为挪干净了,其实没挪完。

通过:

grep -R "listen 80" -n /opt/1panel/apps/openresty/openresty/conf

我最后发现真正藏着 listen 80 的是 00.default.conf 这种默认配置文件。手动把它也改掉之后,再重启 OpenResty,配合:

ss -tulpn | grep -E ':80 |:443 |:8080 |:8443 '

确认:

  • 80 / 443 真正空了;
  • OpenResty 只监听 8080 / 8443;
  • 然后干脆直接把 OpenResty 暂停,为 Dokploy 腾出所有入口端口。

那一刻是“全站彻底下线”的时刻,但也是 Dokploy 安装的窗口期。

3. Dokploy 装上去:Traefik 接管 80/443

ss 显示 80/443/3000 都没人用时,我执行:

curl -sSL https://dokploy.com/install.sh | sh

脚本做了这些事:

  • 安装 Docker(如果没有的话);
  • 初始化 Docker Swarm;
  • 创建 dokploy-network
  • 启动 dokploy(API + UI);
  • 启动 dokploy-traefik 容器,对外绑 80/443

安装完成后:

  • http://服务器IP:3000 打开 Dokploy 面板;
  • ss 显示 80/443 被 docker-proxy(Traefik)占用;
  • 说明 Dokploy 已经成功接管了 公网入口

这时的状态是:

  • Dokploy / Traefik:占据 80/443/3000;
  • OpenResty:停着,所有网站都还处于下线状态。

三、第二轮博弈:Traefik + OpenResty 的“双重 SSL 地狱”

1. 让 OpenResty 回来做“内部服务”

我重新启动了 OpenResty,此时它只监听:

0.0.0.0:8080
0.0.0.0:8443

于是架构变成:

用户 → 80/443 (Traefik) → 8080/8443 (OpenResty) → 后端应用

理论上,只要 Traefik 把各个域名转发到 OpenResty 8080,就能恢复以前所有站点的访问。

我尝试在 Traefik 的动态配置中写一个 openresty.yaml,把多个域名都指向 http://127.0.0.1:8080,比如:

http:
  routers:
    todo:
      rule: Host(`todo.626909.xyz`)
      service: openresty
      entryPoints:

        - web
        - websecure
      tls: {}

  services:
    openresty:
      loadBalancer:
        servers:

          - url: "http://127.0.0.1:8080"

2. HSTS + 双重 SSL:浏览器直接报“你的连接不是私密连接”

问题来了:

  • 我之前用 1Panel + OpenResty 为这些域名申请过 正式的 Let’s Encrypt 证书
  • OpenResty 在 8443 上还保持着 listen 8443 ssl + ssl_certificate 配置;
  • 这些站点还发过 Strict-Transport-Security 响应头,浏览器开启了 HSTS,记住“必须 HTTPS 且证书必须合法”;

现在:

  • 外层:Traefik 用自己的证书(起初是自签之类的)响应;
  • 里层:OpenResty 也在做 SSL;
  • 浏览器一看:证书链不对 + HSTS 还在 → 直接 NET::ERR_CERT_AUTHORITY_INVALID + 不允许继续访问。

于是我以一句非常真实的话总结当时的心情:

“dokploy 套 openresty 双重 ssl。太麻烦了。”

说白了,这一套 Traefik → OpenResty(还在做 SSL) 的结构在概念上是正确的,但要完全调好:

  • 要关掉 OpenResty 的 SSL;
  • 或者让 Traefik 做 TCP 透传;
  • 再和 HSTS、证书、域名解析一起调——代价太大、太容易乱。

那一刻,我做了一个关键决策:

与其维护一个 “Traefik 入口 + OpenResty 反向代理 + 双重 SSL” 的怪兽架构, 不如 逐步弃用 OpenResty,把服务迁到 Dokploy 里统一管理


四、转向:从“反代迁移”到“业务迁移”

这时,我不再纠结“Dokploy 套 OpenResty”这种组合,而是转向一个更干脆的方案:

Dokploy 接管 80/443 → 所有新老服务,慢慢迁移成 Dokploy 管理的服务 → OpenResty 退居二线,直到可以关闭。


五、第一战:迁移 React + Vite 前端项目

目标:把一个 React + TypeScript + Vite 的前端项目迁移上 Dokploy。

1. Build 方式选择

我考虑了几种方式:

  • Static:适合纯静态资源(有独立构建步骤);
  • Dockerfile:最灵活,但要自己写;
  • Nixpacks:自动识别项目类型,按惯例构建,省心。

最终我选了 Nixpacks,让 Dokploy 自动帮我:

  • 安装 Node;
  • npm install + npm run build
  • 用内置的 Caddy/Nginx 之类静态服务器跑起来。

2. 502 Bad Gateway:端口搞错

首次部署完访问,浏览器给了我一个大大的 502 Bad Gateway

排查后发现:

  • 容器里服务监听的是 80
  • 我在 Dokploy 的配置里,把 容器端口/环境变量 PORT 写成了 3000
  • Traefik 试图去访问 容器:3000,结果容器根本没有这个端口开放。

修正办法:

  • 在 Dokploy 面板里,把容器暴露端口改成 80
  • 或者确保项目真正监听的是 Dokploy配置中写的 port。

改完后:

前端项目顺利上线。 我第一次体验到了 “push 一下,CI/CD 完成构建 + 部署 + 域名路由 + HTTPS” 的感觉。


六、第二战:迁移 NewAPI 后端——宿主机桥接模式

第二个目标是一个旧的后端服务 NewAPI

  • 一直跑在宿主机的 4000 端口;
  • 原本由 OpenResty 反代 /some-pathhttp://127.0.0.1:4000

我不想马上把它打包成 Docker,于是选择了一个非常“云原生土办法”的方案:

用一个 Dokploy 管理的容器(socat)做桥接: Traefik → socat(80)→ 宿主机 4000。

1. 核心思路:socat 转发

我在 Dokploy 中创建一个 Compose 服务,跑 alpine/socat

services:
  newapi-bridge:
    image: alpine/socat
    command: >
      socat TCP-LISTEN:80,fork,reuseaddr TCP:host.docker.internal:4000
    extra_hosts:

      - "host.docker.internal:host-gateway"

这意味着:

  • 容器内部 80 端口接收来自 Traefik 的流量;
  • socat 把收到的流量转发到 host.docker.internal:4000(宿主机上 NewAPI 的端口);
  • extra_hosts 让容器里的 host.docker.internal 指向宿主机。

2. 排错:404 / 连接失败 / 搞混了“容器看见的世界”和“宿主机看见的世界”

我曾误判过几次:

  • 宿主机 上敲 curl http://host.docker.internal:4000,发现不通,于是以为“端口不通”;
  • 实际上,这个域名本来就是 容器内用的,宿主机当然不认识;
  • 真正有用的测试是:

  • 在容器里 curl http://host.docker.internal:4000

  • 在宿主机上确认 NewAPI 监听的是 0.0.0.0:4000 而不是 127.0.0.1:4000

后来又踩了一次坑:

  • socat 容器里监听的是 80
  • 我在 Dokploy 的域名路由里,却把“Target port”/“Routing port” 填成了 3000 或其他;
  • Traefik 根本没打到容器的 80。

最终修正:

  • Dokploy 域名配置里,端口改为 80
  • 重启容器确保 extra_hosts 生效;
  • 确保 NewAPI 服务监听的是 0.0.0.0:4000

最后总结这条链路:

用户 → Traefik (443) → socat 容器 (80) → 宿主机 NewAPI (4000)

同时我也彻底理解了:

  • “环境变量里的 PORT 是给应用看”的;
  • “Dokploy / Traefik 的端口配置是给代理路由看的”;
  • 两者完全是两套东西,不要搞混。

七、第三战:Todo 后端与路径反代 vs 子域名

第三个后端服务是 Todo:

  • 旧架构里跑在宿主机 8000
  • 通过 OpenResty 用路径方式暴露,比如 todo.626909.xyz/api/

迁移到 Dokploy 后,我意识到:

  • 继续玩“路径级反代”(/api)会迫使我在 Traefik 上写更复杂的规则(PathPrefix、StripPrefix 等);
  • 在云原生时代,更推荐一个服务一个子域名的方式。

于是我做了一个架构上的决定:

不再用 /api 路径暴露后端, 而是用新的子域名:api.todo.626909.xyz

迁移计划也很清晰:

  1. 后端服务:继续跑在宿主机 8000
  2. Dokploy 再复制一份 socat 桥接配置,从 4000 改为 8000
  3. 绑定一个新的子域名 api.todo.626909.xyz,指向这个 bridge 服务;
  4. 在前端 React 代码中:

  5. 把原来的 fetch('/api/...')

  6. 改为 fetch('https://api.todo.626909.xyz/...')
  7. 重新构建+部署。

这一步完成之后:

Todo 项目前后端就会全部脱离原 OpenResty, 完整挂在 Dokploy / Traefik 体系之下。


八、现状与收官:我真正完成了什么?

截至目前,我完成/规划的是:

  • Dokploy 已经接管 80/443/3000,成为服务器的统一入口;
  • OpenResty 已经从“公网入口”退化为“可有可无”

  • 80/443 不再由它监听;

  • 我有计划逐步把所有站都从它那里迁走;
  • 前端(React + Vite)已经在 Dokploy 内跑起来,构建 & 部署都交给 Nixpacks;
  • NewAPI 后端已经通过 socat bridge 接入 Dokploy
  • 🟡 Todo 后端正在规划用 api.todo.626909.xyz 的方式接入
  • 🟥 双层 SSL/HSTS 的坑已认识清楚:

  • 未来要么完全让 Traefik 管证书;

  • 要么就别再让 OpenResty 抢 HTTPS 入口。

九、这次实战,我学会了这些“底层认知”

  1. Ingress / 入口网关只有一个 80/443 最好只由一个组件统一管理: 要么 OpenResty/Nginx,要么 Traefik/Caddy,混搭很容易出事。

  2. SSL / 证书终止点只能有一个 双重 SSL 不但没必要,还会和 HSTS 搞出一堆奇怪问题。 要么在 Traefik 终止 TLS,要么在 OpenResty 终止,不能两个都上。

  3. 应用端口 vs 代理端口是两回事

  4. PORT 环境变量是应用用来监听的端口;

  5. Traefik/Dokploy 的“Routing Port”是入口网关用来连过去的端口;
  6. 两者不一致就必挂:程序听 80,你路由去 3000,一定挂。

  7. 容器和宿主机看到的世界不一样

  8. 宿主机上的 localhost ≠ 容器里的 localhost

  9. host.docker.internal 是“容器眼里的宿主机”;
  10. 想让容器访问宿主机端口,要么监听 0.0.0.0,要么配 extra_hosts

  11. 与其死扛旧架构,不如接受新入口,逐步迁移业务 一开始我试图让 Dokploy “套” OpenResty,事实证明维护成本和心智负担都偏高。 换个思路,用 Dokploy 作为统一入口,把业务一点点迁进去,思路反而更清晰。